/*
 * This file is part of Euphrat.
 *
 * Copyright © 2012 Bart Verhoeven
 *
 * Euphrat is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Euphrat is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Euphrat. If not, see <http://www.gnu.org/licenses/>.
 */
package org.euphrat.client.controller.login;

import static org.euphrat.client.service.login.LoginContext.*;
import static org.euphrat.client.service.login.LoginContext.State.LOGGED_IN;
import static org.euphrat.client.service.login.LoginContext.State.LOGGED_OUT;
import static org.euphrat.client.service.login.LoginContext.State.LOGIN_FAILED;
import static org.euphrat.client.service.login.LoginContext.State.LOGIN_REQUESTED;


import org.euphrat.client.service.login.LoginContext;
import org.euphrat.client.service.login.LoginService;
import org.euphrat.client.service.login.LoginServiceAsync;

import com.google.gwt.core.client.GWT;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.rpc.IncompatibleRemoteServiceException;
import com.google.gwt.user.client.rpc.InvocationException;

import java.util.HashSet;
import java.util.Set;

/**
 * Controller to regulate login requests. Besides being able to request a login,
 * it provides hooks to be notified of responses to login requests.
 *
 * @author Bart Verhoeven
 */
public class LoginController {

  /** Application-wide login controller. */
  private static LoginController controller;
  /** Login service that performs the actual login requests. */
  private final LoginServiceAsync service = GWT.create(LoginService.class);
  /** All registered login listeners. */
  private final Set<LoginListener> listeners = new HashSet<LoginListener>();
  /** Current login context information. */
  private LoginContext context = new LoginContext();

  /**
   * Returns the application-wide login controller.
   *
   * @return a login controller
   */
  public static synchronized LoginController getInstance() {
    // The application runs as javascript single-threaded code, but I still
    // prefer to add locking to make sure only one instance is created.
    if (controller == null) {
      controller = new LoginController();
    }

    return controller;
  }

  /**
   * Creates a new login controller.
   */
  private LoginController() {
    // Hide constructor: "One controller to rule them all."
    if (controller != null) {
      // Creating a second controller with reflection is not allowed.
      throw new UnsupportedOperationException(
        "Creating multiple instances of LoginController is not supported."
      );
    }
  }

  /**
   * Performs a login request. All {@link #addLoginListener(LoginListener)
   * registered} listeners will be notified of the reply.
   */
  public void login() {
    // Already waiting for a login response.
    if (context.getLoginState() == LOGIN_REQUESTED ||
        context.getLoginState() == LOGGED_IN) {
      return;
    }

    // The actual request.
    String requestURI = Window.Location.getHref();
    service.requestLogin(requestURI, new LoginServiceCallback());

    // Update internal state and notify all listeners.
    LoginContext newContext = new LoginContext();
    newContext.setLoginState(LOGIN_REQUESTED);
    fireLoginChange(newContext);
  }

  /**
   * Registers a login listener to this controller. A listener will only be
   * notified once for each {@link #login() login} request, no matter how many
   * times it has been added.
   *
   * @param listener the login listener to add
   */
  public void addLoginListener(LoginListener listener) {
    if (listener == null) {
      return;
    }

    listeners.add(listener);
  }

  /**
   * Removes a login listener from this controller. It will no longer be
   * notified of responses to login requests.
   *
   * @param listener the login listener to remove
   */
  public void removeLoginListener(LoginListener listener) {
    if (listener == null) {
      return;
    }

    listeners.remove(listener);
  }

  /**
   * Updates the state of the controller and notifies all {@link
   * #addLoginListener(LoginListener) registered} listeners about the change.
   *
   * @param context the new login context
   */
  protected void fireLoginChange(LoginContext context) {
    // Update internal context.
    this.context = context;
    State newState = context.getLoginState();

    // Notify listeners.
    if (newState == LOGGED_IN) {
      for (LoginListener listener : listeners) {
        listener.onLoggedIn(context);
      }
    }
    else if (newState == LOGGED_OUT) {
      for (LoginListener listener : listeners) {
        listener.onLoggedOut(context);
      }
    }
    else if (newState == LOGIN_REQUESTED) {
      for (LoginListener listener : listeners) {
        listener.onLoginRequested(context);
      }
    }
    else if (newState == LOGIN_FAILED) {
      for (LoginListener listener : listeners) {
        listener.onLoginFailed(context);
      }
    }
  }

  /**
   * A callback that receives the reply to a login request. Notifies all
   * registered listeners about the reply.
   */
  private class LoginServiceCallback implements AsyncCallback<LoginContext> {
    @Override
    public void onFailure(Throwable caught) {
      // Determine the cause of the failure and notify user.
      if (caught instanceof IncompatibleRemoteServiceException) {
        Window.alert(
          "Cannot contact login service. Please file a bug report."
        );
      }
      else if (caught instanceof InvocationException) {
        Window.alert(
          "Cannot contact login server. No network connection or server down."
        );
      }

      // Update internal state and notify all listeners of the failure.
      LoginContext newContext = new LoginContext();
      newContext.setLoginState(LOGIN_FAILED);
      fireLoginChange(newContext);
    }

    @Override
    public void onSuccess(LoginContext context) {
      fireLoginChange(context);
    }
  }
}
